<?php
declare(strict_types=1);

namespace ExpressionEngine\ExpressionEngine;

use Contract\Exceptions\LogicException;
use ExpressionEngine\ExpressionEngine\Enum\ExpressionEnum;
use ExpressionEngine\LexicalAnalysisEngine\Enum\LexicalAnalysisEnum;
use ExpressionEngine\LexicalAnalysisEngine\LexicalAnalysisTag;
use ExpressionEngine\OperatorEngine\Enum\OperatorCompareEnum;
use ExpressionEngine\OperatorEngine\SpecialOperator\SpecialOperatorInterface;
use Psr\Container\ContainerInterface;

class ExpressionCompile
{
    const MAP = [
        LexicalAnalysisEnum::TAG_ID_LOOP => ExpressionEnum::COMPILE_ID_LOOP_LIST,
        LexicalAnalysisEnum::TAG_ID_EXPRESSION => ExpressionEnum::COMPILE_ID_EXPRESSION_LIST,
        LexicalAnalysisEnum::TAG_ID_VARIABLE => ExpressionEnum::COMPILE_ID_VARIABLE_LIST,
        LexicalAnalysisEnum::TAG_ID_CONSTANT => ExpressionEnum::COMPILE_ID_CONSTANT_LIST,
    ];

    protected ContainerInterface $container;
    protected LexicalAnalysisTag $lexicalAnalysisTag;

    public function __construct(ContainerInterface $container, LexicalAnalysisTag $lexicalAnalysisTag)
    {
        $this->lexicalAnalysisTag = $lexicalAnalysisTag;
        $this->container = $container;
    }

    /**
     * @param string $expression
     * @param array $compileContext
     * @return mixed
     * @throws LogicException
     */
    public function exec(string $expression, array $compileContext): mixed
    {
        $compileSymbols = [];
        $symbols = explode(' ', $expression);
        foreach ($symbols as $key => $symbol) {
            if ($this->lexicalAnalysisTag->isTag($symbol)) {
                $tagData = $this->lexicalAnalysisTag->parseTag($symbol);
                $compileId = self::MAP[$tagData['tag_id']];
                if (!array_key_exists($tagData['index'], $compileContext[$compileId])) {
                    throw new LogicException('tag:' . $symbol . ' is not valid');
                }
                $compileSymbols[$key] = $this->formatCompileSymbol($compileContext[$compileId][$tagData['index']]);
            } else {
                $compileSymbols[$key] = $symbol;
            }
        }
        $compileExpression = $this->symbolCompile($compileSymbols);
        return $this->getExpressionValue($compileExpression);
    }

    protected function symbolCompile(array $compileSymbols): string
    {
        foreach (OperatorCompareEnum::SPECIAL_LIST as $operator => $operatorClass) {
            if (in_array($operator, $compileSymbols)) {
                /** @var SpecialOperatorInterface $operatorInstance */
                $operatorInstance = $this->container->get($operatorClass);
                $compileSymbols = $operatorInstance->parse($compileSymbols);
            }
        }
        return implode(' ', $compileSymbols);
    }

    /**
     * @param mixed $compileSymbol
     * @return mixed
     */
    protected function formatCompileSymbol(mixed $compileSymbol): mixed
    {
        if (is_string($compileSymbol) && !is_numeric($compileSymbol) && !preg_match('/^\[.+\]$/', $compileSymbol)) {
            $compileSymbol = sprintf("'%s'", $compileSymbol);
        }
        if ($compileSymbol === true) {
            $compileSymbol = 'true';
        } else if ($compileSymbol === false) {
            $compileSymbol = 'false';
        }
        return $compileSymbol;
    }

    /**
     * @param string $compileExpression
     * @return mixed
     */
    protected function getExpressionValue(string $compileExpression): mixed
    {
        $value = '';
        $compileExpression = sprintf('$value = %s;', $compileExpression);
        eval($compileExpression);
        return $value;
    }
}